package gao.xiaolei.service;

import gao.xiaolei.dao.*;
import gao.xiaolei.dto.TestRunInDto;
import gao.xiaolei.dto.TestRunLogDto;
import gao.xiaolei.dto.TestRunOutDto;
import gao.xiaolei.dto.TestTreeDto;
import gao.xiaolei.entity.TestRun;
import gao.xiaolei.entity.TestRunLog;
import gao.xiaolei.entity.TestRunLogStep;
import gao.xiaolei.entity.TestStep;
import gao.xiaolei.exception.AuthException;
import gao.xiaolei.exception.LeafException;
import gao.xiaolei.exception.StatusException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import xiaolei.gao.file.FileInterface;
import gao.xiaolei.util.ServiceName;
import gao.xiaolei.util.UuidUtil;

import javax.annotation.Resource;

import org.springframework.transaction.annotation.Transactional;

import java.nio.charset.StandardCharsets;
import java.time.Clock;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.*;

@Service("TestRunService")
public class TestRunService {

    @Resource(name = "TestRunDao")
    private TestRunDao testRunDao;

    @Resource(name = "TestRunLogDao")
    private TestRunLogDao testRunLogDao;

    @Resource(name = "TestRunLogStepDao")
    private TestRunLogStepDao testRunLogStepDao;

    @Resource(name = "TestStepDao")
    private TestStepDao testStepDao;

    @Resource(name = "FileInterface")
    private FileInterface fileInterface;

    @Autowired
    private RestTemplate restTemplate;

    @Resource(name = "TestStatusDao")
    private TestStatusDao testStatusDao;

    @Resource(name = "TestDesignDao")
    private TestDesignDao testDesignDao;

    @Resource(name = "AnalysisService")
    private AnalysisService analysisService;

    @Transactional
    //添加测试执行
    public TestRunOutDto addTestRun(TestRunInDto testRunInDto) {
        TestRun testRun = testRunInDto.getTestRun();
        if ("root".equals(testRun.getParentId()) && testRun.getLeaf() == 1)
            throw new LeafException("最顶端只能添加测试集！");
        if (!"root".equals(testRun.getParentId()) && testRunDao.getLeaf(testRun.getParentId()) == 1)//如果是添加的父节点本身就是叶子节点的话就报错
            throw new LeafException("添加的父节点为叶子节点！");
        LocalDateTime createDate = LocalDateTime.now(Clock.system(ZoneId.of("Asia/Shanghai")));
        testRun.setCreateDate(createDate);
        testRun.setUpdateDate(createDate);
        testRunDao.save(testRun);//如果添加的是测试集文件夹,就不用去添加测试执行记录，也不用拉去审核
        TestRunOutDto result = testRun.getLeaf() == 1 ? null : getDetail(testRun.getId());//如果是叶子节点的话就靠下面的来获取
        if (testRun.getLeaf() == 1) {//但如果添加的是测试执行就要去添加测试执行记录，还要决定要不要拉去审核
            List<TestRunLogDto> testRunLogDtoList = testRunInDto.getTestRunLogDto();
            for (TestRunLogDto testRunLogDto : testRunLogDtoList) {
                testRunLogDto.getTestRunLog().setCreateDate(createDate);//添加测试执行记录的生成时间
                testRunLogDto.getTestRunLog().setTestRunId(testRun.getId());//添加测试执行记录的的所属测试执行
                testRunLogDao.save(testRunLogDto.getTestRunLog());//保存测试执行记录
                List<TestRunLogStep> inRunLogSteps = testRunLogDto.getTestRunLogStep();//获取前端传过来的测试步骤
                List<TestStep> oldSteps = testStepDao.findByTestDesign(testRun.getTestDesign());
                saveNewRunLogStep(oldSteps, inRunLogSteps, testRun.getProject(), testRunLogDto.getTestRunLog().getId());
            }
            result = getDetail(testRun.getId());
        }
        return result;
    }

    @Transactional
    //删除测试执行
    public void deleteTestRun(String id, String userId) {
        if (!testRunDao.getAuthor(id).orElse("").equals(userId))
            throw new AuthException("你无此权限删除！");
        if (testRunDao.getStatus(id) == 2)
            throw new StatusException("审核中的节点不能删除");
        String project = testRunDao.getProject(id);
        List<String> willDeletedFile = new LinkedList<>();//防止数据库回退但是文件操作依旧执行
        recurrenceDelete(id, willDeletedFile);
        analysisService.doAnalysis(testRunDao.getProject(id));//更新测试需求分析
        for (String df : willDeletedFile) {
            fileInterface.fileDelete(df, project);
        }
    }

    @Transactional
    //修改测试执行
    public TestRunOutDto updateTestRun(TestRunInDto testRunInDto) {
        String userId = testRunInDto.getUserId();
        TestRun testRun = testRunInDto.getTestRun();
        if (!testRunDao.existsById(testRun.getId()))
            throw new IllegalArgumentException("该测试执行不存在！");
        if (!testRunDao.getAuthor(testRun.getId()).orElse("").equals(userId))
            throw new AuthException("你无此权限修改！");
        if (testRunDao.getStatus(testRun.getId()) == 2)
            throw new StatusException("审核中的节点不能修改");
        LocalDateTime updateDate = LocalDateTime.now(Clock.system(ZoneId.of("Asia/Shanghai")));
        LocalDateTime createDate = testRunDao.getCreateDate(testRun.getId());
        testRun.setUpdateDate(updateDate);
        testRun.setCreateDate(createDate);
        testRunDao.save(testRun);
        TestRunOutDto result = testRun.getLeaf() == 1 ? null : getDetail(testRun.getId());//如果是叶子节点的话就靠下面的来获取
        if (testRun.getLeaf() == 1) {//如果不是测试集就要执行下面的操作
            List<String> willDeletedFile = new LinkedList<>();//防止数据库回退但是文件操作依旧执行
            Map<String, String> willUplodeFile = new HashMap<>();//防止数据库回退但是文件操作依旧执行
            List<TestRunLog> oldLog = testRunLogDao.findByTestRunId(testRun.getId());
            List<TestRunLogDto> newLog = testRunInDto.getTestRunLogDto();
            for (TestRunLog ol : oldLog) {
                boolean flag = true;//判断这个旧测试执行步骤是否要删除
                for (TestRunLogDto nl : newLog) {
                    if (ol.getId().equals(nl.getTestRunLog().getId())) {
                        flag = false;
                        break;
                    }
                }
                if (flag) {//如果新执行记录里没有就说明这个旧执行记录要删除，所以要去文件服务删除
                    List<TestRunLogStep> testRunLogSteps = testRunLogStepDao.findByTestRunLog(ol.getId());
                    for (int i = 0, length = testRunLogSteps.size(); i < length; i++) {
                        willDeletedFile.add(testRunLogSteps.get(i).getId());//删除测试步骤的内容文件
                        willDeletedFile.add(testRunLogStepDao.getReasonId(testRunLogSteps.get(i).getId()));//删除测试步骤的判断理由文件
                        testRunLogStepDao.deleteById(testRunLogSteps.get(i).getId());//删除测试步骤本身
                    }
                    testRunLogDao.deleteById(ol.getId());//最后再删除测试执行记录本身
                }
            }
            for (TestRunLogDto nl : newLog) {//保存新测试执行记录
                nl.getTestRunLog().setTestRunId(testRun.getId());//添加测试执行记录的的所属测试执行
                if (nl.getIsCreate() != null && nl.getIsCreate() == 1) {//添加测试执行记录的生成时间//如果是新添加的执行记录,就要各种复杂的保存副本操作
                    nl.getTestRunLog().setCreateDate(updateDate);//只有新建的执行记录才要写入创建时间
                    saveNewRunLogStep(testStepDao.findByTestDesign(testRun.getTestDesign()), nl.getTestRunLogStep(), testRun.getProject(), nl.getTestRunLog().getId());
                } else {//如果是本来就有的执行记录
                    for (TestRunLogStep testRunLogStep : nl.getTestRunLogStep()) {
                        nl.getTestRunLog().setCreateDate(testRunLogDao.getCreateDate(nl.getTestRunLog().getId()));
                        testRunLogStepDao.updateIsPass(testRunLogStep.getIsPass(), testRunLogStep.getId());//就只要修改执行结果就好了
                        willUplodeFile.put(testRunLogStepDao.getReasonId(testRunLogStep.getId()), testRunLogStep.getReason());//将每个测试步骤的理由都更新一下
                    }
                }
                testRunLogDao.save(nl.getTestRunLog());//保存测试执行记录本身
            }
            analysisService.doAnalysis(testRun.getProject());//更新测试需求分析
            result = getDetail(testRun.getId());
            for (String df : willDeletedFile) {
                fileInterface.fileDelete(df, testRun.getProject());//删除测试步骤的内容文件和测试步骤的判断理由文件
            }
            for (Map.Entry<String, String> uf : willUplodeFile.entrySet()) {
                fileInterface.fileUpload(uf.getValue(), uf.getKey(), testRun.getProject());//将每个测试步骤的理由都更新一下
            }
        }
        return result;
    }

    //修改测试执行的标题
    @Transactional
    public void updateTitle(String id, String title, String userId) {
        int status = testRunDao.getStatus(id);
        if (!testRunDao.getAuthor(id).orElse("").equals(userId))
            throw new AuthException("你无此权限修改！");
        if (status == 2)
            throw new StatusException("审核中的节点不能修改");
        testRunDao.updateTitle(id, title);
        testRunDao.updateStatus(id, 0);//如果本身就是审核通过或者说审核不通过的话修改了就要变为待审核
        analysisService.doAnalysis(testRunDao.getProject(id));//更新测试需求分析
    }


    // 修改父节点
    @Transactional
    public void updateParentId(String id, String newParentId, String userId) {
        int status = testRunDao.getStatus(id);
        if (!testRunDao.getAuthor(id).orElse("").equals(userId))
            throw new AuthException("你无此权限移动！");
        if (status == 2)
            throw new StatusException("审核中的节点不能移出或移入！");
        if (testRunDao.getLeaf(newParentId) == 1)
            throw new LeafException("只能移入文件夹！");
        testRunDao.updateParentId(id, newParentId);
        testRunDao.updateStatus(id, 0);//如果本身就是审核通过或者说审核不通过的话修改了就要变为待审核
        analysisService.doAnalysis(testRunDao.getProject(id));//更新测试需求分析
    }


    //根据项目和作者来查询所有最顶端测试集(一个人只能看到所有测试集以及自己写的测试执行和别人已经审核通过了的)
    @Transactional(readOnly = true)
    public List<TestTreeDto> getRootTestLab(String projectId, String userId) {
        List<Object[]> os = testRunDao.findByProjectAndParentIdOrderByTitle(projectId, "root");
        return getTestTreeDtos(os, userId, 0);
    }

    //根据项目查询所有最顶端主题文件夹(给测试执行提供关联测试计划的选择（只提供审核通过的测试计划）)
    @Transactional(readOnly = true)
    public List<TestTreeDto> getRootTestLabIsPass(String projectId) {
        List<Object[]> os = testRunDao.findByProjectAndParentIdOrderByTitle(projectId, "root");
        return getTestTreeDtos(os, null, 2);
    }

    //根据项目来查询所有最顶端测试集(为了给测试计划转测试执行选择测试集)
    @Transactional(readOnly = true)
    public List<TestTreeDto> getRootTestLabForTransform(String projectId) {
        List<Object[]> os = testRunDao.findByProjectAndParentIdOrderByTitleForTransform(projectId, "root");
        return getTestTreeDtos(os, null, 1);
    }

    //根据作者来获取某个节点的所有能显示的节点(一个人只能看到自己写的和别人已经审核通过了的)
    @Transactional(readOnly = true)
    public List<TestTreeDto> getChildren(String id, String userId) {
        List<Object[]> os = testRunDao.getChildren(id, userId);
        return getTestTreeDtos(os, userId, 0);
    }

    //获取某个节点下是文件夹或者通过审核的节点
    @Transactional(readOnly = true)
    public List<TestTreeDto> getChildrenIsPass(String id) {
        List<Object[]> os = testRunDao.getChildrenIsPass(id);
        return getTestTreeDtos(os, null, 2);
    }

    //获取某个节点下的所有测试集文件夹
    @Transactional(readOnly = true)
    public List<TestTreeDto> getChildrenIsTestLab(String id) {
        List<Object[]> os = testDesignDao.getChildrenIsFolder(id);
        return getTestTreeDtos(os, null, 1);
    }

    // 获取该测试执行的详细信息
    @Transactional(readOnly = true)
    public TestRunOutDto getDetail(String id) {
        TestRun testRun = testRunDao.findById(id).orElseThrow(() -> new IllegalArgumentException("该测试执行不存在！"));
        String author = restTemplate.getForObject(ServiceName.AUTH_SERVICE.getServiceName().concat("/user/realName/").concat(testRun.getAuthor()), String.class);
        if (testRun.getLeaf() == 0)//如果是主题文件夹就只要获取描述信息和作者就好了
            return new TestRunOutDto(testRun.getTitle(), author, testRun.getAuthor(), testRun.getDescribe(), testRun.getCreateDate(), testRun.getUpdateDate(), null);
        //不然就要把测试执行记录拿出来
        String status = testStatusDao.getDescribeOfCode(testRun.getStatus());
        List<TestRunLog> testRunLogs = testRunLogDao.findByTestRunIdOrderByCreateDateDesc(id);
        List<TestRunLogDto> testRunLogDtoList = new ArrayList<>(testRunLogs.size());
        for (int i = 0, length = testRunLogs.size(); i < length; i++) {//将测试执行记录获取出来封装到Dto中
            List<TestRunLogStep> steps = testRunLogStepDao.findByTestRunLog(testRunLogs.get(i).getId());
            for (int j = 0, length2 = steps.size(); j < length2; j++) {//将执行步骤都获取出来
                String stepText = new String(fileInterface.getFile(steps.get(j).getId(), testRun.getProject()), StandardCharsets.UTF_8);
                String reasonText = new String(fileInterface.getFile(steps.get(j).getReasonId(), testRun.getProject()), StandardCharsets.UTF_8);
                steps.get(j).setDescription(stepText);
                steps.get(j).setReason(reasonText);
            }
            testRunLogDtoList.add(new TestRunLogDto(testRunLogs.get(i), steps));
        }
        return new TestRunOutDto(testRun.getTitle(), author, testRun.getAuthor(), status, testRun.getCreateDate(), testRun.getUpdateDate(), testRun.getDescribe(), testRun.getPriority(), testRun.getTestDesign(), getTestDesignPath(testRun.getTestDesign()), testRunLogDtoList, null);
    }

    //获取对应测试运行的标题
    public String getTitle(String id) {
        return testRunDao.getTitle(id);
    }

    //用于递归删除子节点的方法
    private void recurrenceDelete(String id, List<String> willDeletedFile) {
        int isLeaf = testRunDao.getLeaf(id);
        if (isLeaf == 0) {//如果不是叶子节点则遍历它的所有子节点来递归删除
            List<String> cls = testRunDao.getChildrenId(id);
            for (int i = 0, length = cls.size(); i < length; i++) {
                recurrenceDelete(cls.get(i), willDeletedFile);
            }
        }
        if (isLeaf == 1) {//如果是删除测试集就不用去删除下面这些测试步骤，只要删除自己就好了
            List<TestRunLog> testRunLogs = testRunLogDao.findByTestRunId(id);
            for (int i = 0, length = testRunLogs.size(); i < length; i++) {
                List<TestRunLogStep> steps = testRunLogStepDao.findByTestRunLog(testRunLogs.get(i).getId());
                for (int j = 0, length2 = steps.size(); j < length2; j++) {
                    willDeletedFile.add(steps.get(j).getId());//删除测试步骤的内容文件
                    willDeletedFile.add(steps.get(j).getReasonId());//删除测试步骤的判断理由文件
                    testRunLogStepDao.deleteById(steps.get(j).getId());//删除除测试步骤本身
                }
                testRunLogDao.deleteById(testRunLogs.get(i).getId());//删除测试执行记录
            }
        }
        testRunDao.deleteById(id);//最后把测试执行删除
    }

    //将List<Object[]>转换成List<TestTreeDto>
    private List<TestTreeDto> getTestTreeDtos(List<Object[]> os, String userId, int type) {
        List<TestTreeDto> runTreeDtos = new ArrayList<>(os.size());
        for (int i = 0, length = os.size(); i < length; i++) {
            int count = 0;
            if ((Integer) os.get(i)[3] == 0) {//如果该节点不是叶子节点的话
                if (type == 0)//供测试执行自己的树形菜单使用
                    count = testRunDao.getChildrenCount((String) os.get(i)[0], userId);//获取这个节点下能够显示的节点数量
                else if (type == 1)///供测试计划转测试执行用
                    count = testRunDao.getChildrenCountIsFolder((String) os.get(i)[0]);
                else//或者通过审核或者是文件夹的子节点数量
                    count = testRunDao.getChildrenCountIsPass((String) os.get(i)[0]);//获取这个节点下能够显示的节点数量
            }
            runTreeDtos.add(new TestTreeDto((String) os.get(i)[0], (String) os.get(i)[1], (String) os.get(i)[2], (Integer) os.get(i)[3], type > 0 ? null : (String) os.get(i)[4], count, type > 0 ? null : (Integer) os.get(i)[5]));
        }
        return runTreeDtos;
    }

    //用于将新建的测试执行记录下的步骤持久化
    private void saveNewRunLogStep(List<TestStep> oldSteps, List<TestRunLogStep> inRunLogSteps, String projectId, String testRunLog) {
        Map<String, String> willCopyFile = new HashMap<>(oldSteps.size());//防止数据库回退但是文件操作依旧执行
        Map<String, String> willUploadFile = new HashMap<>(oldSteps.size());//防止数据库回退但是文件操作依旧执行
        List<TestRunLogStep> runLogSteps = new ArrayList<>(oldSteps.size());//用于储存步骤副本
        for (int i = 0, length = oldSteps.size(); i < length; i++) {//在原本测试步骤的基础上添加测试执行的信息形成副本
            TestStep oldStep = oldSteps.get(i);
            TestRunLogStep runLogStep = null;
            for (TestRunLogStep step : inRunLogSteps) {
                if (oldStep.getId().equals(step.getId())) {
                    runLogStep = step;
                    break;
                }
            }
            runLogSteps.add(new TestRunLogStep(UuidUtil.get32Uuid(), oldStep.getName(), oldStep.getFrontNode(), oldStep.getBackNode(), oldStep.getExpectedResult(), testRunLog, runLogStep.getIsPass(), UuidUtil.get32Uuid(), runLogStep.getReason()));
        }
        for (int i = 0, length = runLogSteps.size(); i < length; i++) {//将要保存的测试执行的步骤对比测试计划的步骤来修改frontNode和backNode
            String frontNode = runLogSteps.get(i).getFrontNode();
            String backNode = runLogSteps.get(i).getBackNode();
            if (!"begin".equals(frontNode)) {
                for (int j = 0, length2 = oldSteps.size(); j < length2; j++) {
                    if (frontNode.equals(oldSteps.get(j).getId())) {
                        runLogSteps.get(i).setFrontNode(runLogSteps.get(j).getId());
                        break;
                    }
                }
            }
            if (!"end".equals(backNode)) {
                for (int j = 0, length2 = oldSteps.size(); j < length2; j++) {
                    if (backNode.equals(oldSteps.get(j).getId())) {
                        runLogSteps.get(i).setBackNode(runLogSteps.get(j).getId());
                        break;
                    }
                }
            }
            testRunLogStepDao.save(runLogSteps.get(i));//保存每个执行记录的测试步骤(即将目前的测试计划的测试步骤做一个副本，以防以后改了测试步骤会波及到以前的测试执行)以及测试步骤的执行结果
            willCopyFile.put(runLogSteps.get(i).getId(), oldSteps.get(i).getId());//测试步骤的内容也要做一份副本
            willUploadFile.put(runLogSteps.get(i).getReasonId(), runLogSteps.get(i).getReason());//测试步骤的成功或失败的判断理由也要保存
        }
        for (Map.Entry<String, String> cf : willCopyFile.entrySet()) {
            fileInterface.copyFile(projectId, cf.getValue(), projectId, cf.getKey());//测试步骤的内容也要做一份副本
        }
        for (Map.Entry<String, String> uf : willUploadFile.entrySet()) {
            fileInterface.fileUpload(uf.getValue(), uf.getKey(), projectId);//测试步骤的成功或失败的判断理由也要保存
        }
    }

    //获取某一个测试计划的路径
    private String getTestDesignPath(String id) {
        List<String> path = new LinkedList<>();
        String parentId = testDesignDao.getParentId(id);
        while (!"root".equals(parentId) && parentId != null) {
            path.add(testDesignDao.getTitle(parentId));
            parentId = testDesignDao.getParentId(parentId);
        }
        StringBuilder result = new StringBuilder();
        for (int i = path.size() - 1; i >= 0; i--) {
            result.append(path.get(i)).append("/");
        }
        result.append(testDesignDao.getTitle(id));
        return result.toString();
    }
}
